/**
 * Copyright (c) 2018-2023, NXOS Development Team
 * SPDX-License-Identifier: Apache-2.0
 *
 * Contains: Implementation of split virtio queue.
 *
 * Change Logs:
 * Date           Author            Notes
 * 2023-07-13     Shiroko           Init
 */
#include <nxos.h>

#ifdef CONFIG_NX_DRIVER_ENABLE_VIRTIO

#define NX_LOG_NAME "VIRTIO-QUEUE"
#define NX_LOG_LEVEL NX_LOG_DBG

#include <base/log.h>

#include "virtio.h"

void VirtioQueueAlloc(NX_Device *device, int queue_num)
{
    NX_U16 max_queue                      = 0;
    VirtioCommonConfigGet(device, VIRTIO_COMMON_CFG_QUEUE_NUM_MAX, &max_queue);
    NX_ASSERT(max_queue > 0);
    NX_ASSERT(queue_num <= max_queue);

    VirtioQueue *queue = NX_MemAlloc(sizeof(VirtioQueue) * queue_num);
    NX_ASSERT(queue != NX_NULL);
    VirtioGetExtension(device)->queue     = queue;
    VirtioGetExtension(device)->queue_num = queue_num;
}

void VirtioQueueFree(NX_Device *device)
{
    VirtioQueue *queue = VirtioGetExtension(device)->queue;
    if (queue != NX_NULL)
    {
        VirtioGetExtension(device)->queue     = NX_NULL;
        VirtioGetExtension(device)->queue_num = 0;
        NX_MemFree(queue);
    }
}

void VirtioQueueInit(NX_Device *device, NX_U16 queue_index, NX_U16 ring_size)
{
    VirtioExtension *deviceExtension = VirtioGetExtension(device);

    // Guard queue index
    NX_ASSERT(queue_index < deviceExtension->queue_num);
    NX_ASSERT(((ring_size - 1) & ring_size) == 0);

    // A queue consists of desc, avail and used.
    VirtioQueue *queue = &deviceExtension->queue[queue_index];
    queue->queue_index = queue_index;

    // Calculate size requirement
    NX_Size queue_size =
                    NX_ALIGN_UP(
                            VIRTIO_QUEUE_DESC_TOTAL_SIZE(ring_size) + VIRTIO_QUEUE_AVAIL_TOTAL_SIZE(ring_size),
                            CONFIG_NX_DRIVER_VIRTIO_QUEUE_USED_ALIGNMENT
                    ) + VIRTIO_QUEUE_USED_TOTAL_SIZE(ring_size);
    queue_size = NX_PAGE_ALIGNUP(queue_size);

    // Allocate memory of a queue
    void *pages = NX_PageAlloc(queue_size / NX_PAGE_SIZE);
    NX_ASSERT(pages != NX_NULL);
    NX_MemSet(pages, 0, queue_size);
    VirtioQueueTrace *trace = (VirtioQueueTrace *) NX_MemAlloc(ring_size * sizeof(VirtioQueueTrace));
    NX_ASSERT(trace != NX_NULL);
    NX_Bool *free = (NX_Bool *) NX_MemAlloc(ring_size * sizeof(NX_Bool));
    NX_ASSERT(free != NX_NULL);
    for (int i = 0; i < ring_size; i++)
    {
        free[i] = NX_True;
        trace[i].ok      = NX_False;
        trace[i].waiting = NX_False;
        NX_SemaphoreInit(&trace[i].lock, 1);
    }

    // Setup queue pointer
    queue->desc  = (VirtioQueueDesc *) pages;
    queue->avail = (VirtioQueueAvail *) (pages + VIRTIO_QUEUE_DESC_TOTAL_SIZE(ring_size));
    queue->used  = (VirtioQueueUsed *) (pages +
                                        NX_ALIGN_UP(
                                                VIRTIO_QUEUE_DESC_TOTAL_SIZE(ring_size) +
                                                VIRTIO_QUEUE_AVAIL_TOTAL_SIZE(ring_size),
                                                CONFIG_NX_DRIVER_VIRTIO_QUEUE_USED_ALIGNMENT
                                        ));

    queue->ring_size  = ring_size;
    queue->free       = free;
    queue->free_count = ring_size;
    queue->trace      = trace;
    queue->used_idx   = 0;

    // Initialize lock
    NX_MutexInit(&queue->freeLock);
    NX_MutexInit(&queue->availRingLock);
    NX_SpinInit(&queue->usedRingLock);

    // Negotiate this queue with device
    NX_U16 enable = VIRTIO_COMMON_CFG_QUEUE_ENABLE_DISABLE;
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_QUEUE_SELECT, &queue_index);
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_QUEUE_ENABLE, &enable);
    NX_U64 addr = (NX_U64) ((NX_UArch) (NX_Virt2Phy(queue->desc)));
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_QUEUE_DESC, &addr);
    addr = (NX_U64) ((NX_UArch) NX_Virt2Phy(queue->avail));
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_QUEUE_DRIVER, &addr);
    addr = (NX_U64) ((NX_UArch) NX_Virt2Phy(queue->used));
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_QUEUE_DEVICE, &addr);
    enable = VIRTIO_COMMON_CFG_QUEUE_ENABLE_ENABLE;
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_QUEUE_ENABLE, &enable);
    // Or we can fetch maximum queue size support by device and use it.
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_QUEUE_SIZE, &ring_size);
}

void VirtioQueueDestroy(NX_Device *device, NX_U16 queue_index)
{
    VirtioExtension *deviceExtension = VirtioGetExtension(device);
    NX_ASSERT(queue_index < deviceExtension->queue_num);
    VirtioQueue *queue = &deviceExtension->queue[queue_index];

    // Negotiate this queue with device
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_QUEUE_SELECT, &queue_index);
    NX_U64 addr = (NX_U64) ((NX_UArch) NX_NULL);
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_QUEUE_DESC, &addr);
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_QUEUE_DRIVER, &addr);
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_QUEUE_DEVICE, &addr);

    // Free resource
    NX_MemFree(queue->free);
    NX_MemFree(queue->trace);
    NX_PageFree(queue->desc);

    NX_MemSet((NX_U8 *) queue, 0, sizeof(VirtioQueue));
}

VirtioQueue *VirtioQueueGet(NX_Device *device, NX_U16 queue_index)
{
    VirtioExtension *deviceExtension = VirtioGetExtension(device);
    NX_ASSERT(queue_index < deviceExtension->queue_num);
    return &deviceExtension->queue[queue_index];
}

NX_U32 VirtioQueueDescAlloc(VirtioQueue *queue)
{
    NX_MutexLock(&queue->freeLock);
    NX_U32 idx;
    if (queue->free_count == 0) goto failed;
    for (idx = 0; idx < queue->ring_size; idx++)
        if (queue->free[idx] == NX_True)
        {
            queue->free[idx] = NX_False;
            queue->free_count--;
            NX_MutexUnlock(&queue->freeLock);
            return idx;
        }
failed:
    NX_MutexUnlock(&queue->freeLock);
    return VIRTIO_QUEUE_DESC_INVALID_ID;
}

NX_Error VirtioQueueDescAllocMany(VirtioQueue *queue, int num, NX_U32 *allocated_ids)
{
    NX_ASSERT(allocated_ids != NX_NULL);
    NX_MutexLock(&queue->freeLock);
    if (queue->free_count < num)
        goto failed;
    int      i   = 0;
    // We have the guarantee of successfully allocating
    for (int idx = 0; idx < queue->ring_size; idx++)
    {
        if (queue->free[idx] == NX_True)
        {
            allocated_ids[i++] = idx;
            queue->free[idx]   = NX_False;
            queue->free_count--;
            if (i == num) break;
        }
    }
    NX_MutexUnlock(&queue->freeLock);
    return NX_EOK;
failed:
    NX_MutexUnlock(&queue->freeLock);
    return NX_ENORES;
}

NX_PRIVATE void VirtioQueueDescFreeNoLock(VirtioQueue *queue, NX_U32 id)
{
    NX_ASSERT(queue->ring_size > id);
    NX_ASSERT(queue->free[id] == NX_False);
    queue->desc[id].addr  = 0;
    queue->desc[id].len   = 0;
    queue->desc[id].flags = 0;
    queue->desc[id].next  = 0;
    queue->free[id] = NX_True;
    queue->free_count++;
}


void VirtioQueueDescFree(VirtioQueue *queue, NX_U32 id)
{
    NX_MutexLock(&queue->freeLock);
    VirtioQueueDescFreeNoLock(queue, id);
    NX_MutexUnlock(&queue->freeLock);
}

void VirtioQueueDescFreeMany(VirtioQueue *queue, NX_U32 id)
{
    NX_MutexLock(&queue->freeLock);
    for (;;)
    {
        NX_ASSERT(queue->ring_size > id);
        NX_U16 flags = queue->desc[id].flags;
        NX_U16 next  = queue->desc[id].next;
        VirtioQueueDescFreeNoLock(queue, id);
        if (flags & VIRTIO_QUEUE_DESC_FLAG_NEXT)
            id = next;
        else
            break;
    }
    NX_MutexUnlock(&queue->freeLock);
}

VirtioQueueDesc *VirtioQueueDescGet(VirtioQueue *queue, NX_U32 id)
{
    NX_ASSERT(queue->ring_size > id);
    return &queue->desc[id];
}


void VirtioQueueDescFill(VirtioQueue *queue, NX_U32 desc_id, NX_U64 addr, NX_U32 len, NX_U16 flags, NX_U16 next)
{
    queue->desc[desc_id].addr  = addr;
    queue->desc[desc_id].len   = len;
    queue->desc[desc_id].flags = flags;
    queue->desc[desc_id].next  = next;
}

void VirtioQueuePutIntoAvailRing(VirtioQueue *queue, NX_U32 desc_id)
{
    NX_MutexLock(&queue->availRingLock);
    queue->avail->ring[queue->avail->idx % queue->ring_size] = desc_id;
    NX_MemoryBarrier();
    queue->avail->idx++;
    NX_MutexUnlock(&queue->availRingLock);
}

void VirtioQueueRegisterNotifier(VirtioQueue *queue, VirtioQueueCompletedHandler handler)
{
    queue->notifier = handler;
}

void VirtioQueueUnregisterNotifier(VirtioQueue *queue, VirtioQueueCompletedHandler handler)
{
    queue->notifier = NX_NULL;
}

#endif // CONFIG_NX_DRIVER_ENABLE_VIRTIO
